package com.afc.struts.form.generator;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Vector;

import net.htmlparser.jericho.Element;
import net.htmlparser.jericho.Source;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import com.afc.struts.attribute.Attribute;
import com.afc.struts.attribute.Attribute.DefaultAttribute;
import com.afc.struts.utils.Constants;
import com.afc.struts.utils.StringUtils;

public interface FormGenerator
{
  public void generateForm(String aName, InputStream anInputStream, OutputStream anOutputStream) throws IOException;

  public class JerichoFormGenerator implements FormGenerator
  {
    @Override
    public void generateForm(String aName, InputStream anInputStream, OutputStream anOutputStream) throws IOException
    {
      Collection<Attribute> formAttributes = getFormAttributes(anInputStream);
      ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
      classWriter.visit(51, Opcodes.ACC_PUBLIC, aName, null, AsmUtils.OBJECT_CLASS_NAME, new String[]
        {
          FormGeneratorUtils.ACTION_FORM_CLASS_NAME
        });

      MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, FormGeneratorUtils.ACTION_FORM_VALIDATE_API.getName(), FormGeneratorUtils.ACTION_FORM_VALIDATE_API.getDescription(), null, null);
      methodVisitor.visitCode();
      methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, FormGeneratorUtils.LABELED_BOOLEAN_CLASS_NAME, "OK", FormGeneratorUtils.LABELED_BOOLEAN_REFERENCE_NAME);
      methodVisitor.visitInsn(Opcodes.ARETURN);
      methodVisitor.visitMaxs(0, 0);
      methodVisitor.visitEnd();

      for (Attribute attribute : formAttributes)
      {
        visitAttribute(aName, attribute, classWriter);
      }

      MethodVisitor constructorVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, AsmUtils.CONSTRUCTOR_NAME, "()V", null, null);
      constructorVisitor.visitCode();
      constructorVisitor.visitVarInsn(Opcodes.ALOAD, 0);
      constructorVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, AsmUtils.OBJECT_CLASS_NAME, AsmUtils.CONSTRUCTOR_NAME, "()V");
      for (Attribute attribute : formAttributes)
      {
        visitConstructor(aName, attribute, classWriter, constructorVisitor);
      }
      constructorVisitor.visitInsn(Opcodes.RETURN);
      constructorVisitor.visitMaxs(0, 0);
      constructorVisitor.visitEnd();

      classWriter.visitEnd();
      anOutputStream.write(classWriter.toByteArray());
    }

    protected void visitAttribute(String anOwner, Attribute anAttribute, ClassVisitor aClassVisitor)
    {
      String referenceType = AsmUtils.getReferenceName(AsmUtils.fromHighLevelClassName(anAttribute.getType().getName()));

      FieldVisitor fieldVisitor = aClassVisitor.visitField(Opcodes.ACC_PRIVATE, anAttribute.getName(), referenceType, null, null);
      fieldVisitor.visitEnd();

      MethodVisitor getterVisitor = aClassVisitor.visitMethod(Opcodes.ACC_PUBLIC, "get" + StringUtils.toUpperCaseFirstChar(anAttribute.getName()), "()" + referenceType, null, null);
      String dao = anAttribute.getProperty("dao");
      if (dao != null)
      {
        AnnotationVisitor annotationVisitor = getterVisitor.visitAnnotation(FormGeneratorUtils.POSSIBLE_VALUES_REFERENCE_NAME, true);
        annotationVisitor.visit("value", "get" + StringUtils.toUpperCaseFirstChar("possible" + StringUtils.toUpperCaseFirstChar(anAttribute.getName()) + "s"));
        annotationVisitor.visitEnd();
      }
      getterVisitor.visitCode();
      getterVisitor.visitVarInsn(Opcodes.ALOAD, 0);
      getterVisitor.visitFieldInsn(Opcodes.GETFIELD, anOwner, anAttribute.getName(), referenceType);
      getterVisitor.visitInsn(AsmUtils.getReturnOpcode(anAttribute.getType().getName()));
      getterVisitor.visitMaxs(0, 0);
      getterVisitor.visitEnd();

      MethodVisitor setterVisitor = aClassVisitor.visitMethod(Opcodes.ACC_PUBLIC, "set" + StringUtils.toUpperCaseFirstChar(anAttribute.getName()), "(" + referenceType + ")V", null, null);
      setterVisitor.visitCode();
      setterVisitor.visitVarInsn(Opcodes.ALOAD, 0);
      setterVisitor.visitVarInsn(AsmUtils.getLoadOpcode(anAttribute.getType().getName()), 1);
      setterVisitor.visitFieldInsn(Opcodes.PUTFIELD, anOwner, anAttribute.getName(), referenceType);
      setterVisitor.visitInsn(Opcodes.RETURN);
      setterVisitor.visitMaxs(0, 0);
      setterVisitor.visitEnd();
    }

    protected void visitConstructor(String anOwner, Attribute anAttribute, ClassVisitor aClassVisitor, MethodVisitor aConstructorVisitor)
    {
      String dao = anAttribute.getProperty("dao");
      if (dao != null)
      {
        String possibleValuesFieldName = "possible" + StringUtils.toUpperCaseFirstChar(anAttribute.getName()) + "s";
        FieldVisitor possibleValuesFieldVisitor = aClassVisitor.visitField(Opcodes.ACC_PRIVATE, possibleValuesFieldName, FormGeneratorUtils.LIST_REFERENCE_NAME, null, null);
        possibleValuesFieldVisitor.visitEnd();

        String getterName = "get" + StringUtils.toUpperCaseFirstChar(possibleValuesFieldName);
        MethodVisitor getterVisitor = aClassVisitor.visitMethod(Opcodes.ACC_PUBLIC, getterName, "()" + FormGeneratorUtils.LIST_REFERENCE_NAME, null, null);
        getterVisitor.visitCode();
        getterVisitor.visitVarInsn(Opcodes.ALOAD, 0);
        getterVisitor.visitFieldInsn(Opcodes.GETFIELD, anOwner, possibleValuesFieldName, FormGeneratorUtils.LIST_REFERENCE_NAME);
        getterVisitor.visitInsn(Opcodes.ARETURN);
        getterVisitor.visitMaxs(0, 0);
        getterVisitor.visitEnd();

        aConstructorVisitor.visitVarInsn(Opcodes.ALOAD, 0);
        aConstructorVisitor.visitTypeInsn(Opcodes.NEW, FormGeneratorUtils.VECTOR_CLASS_NAME);
        aConstructorVisitor.visitInsn(Opcodes.DUP);
        aConstructorVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, FormGeneratorUtils.VECTOR_CLASS_NAME, AsmUtils.CONSTRUCTOR_NAME, "()V");
        aConstructorVisitor.visitFieldInsn(Opcodes.PUTFIELD, anOwner, possibleValuesFieldName, FormGeneratorUtils.LIST_REFERENCE_NAME);
        aConstructorVisitor.visitVarInsn(Opcodes.ALOAD, 0);
        aConstructorVisitor.visitFieldInsn(Opcodes.GETFIELD, anOwner, possibleValuesFieldName, FormGeneratorUtils.LIST_REFERENCE_NAME);
        aConstructorVisitor.visitInsn(Opcodes.ACONST_NULL);
        aConstructorVisitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, FormGeneratorUtils.LIST_CLASS_NAME, FormGeneratorUtils.COLLECTION_ADD_API.getName(), FormGeneratorUtils.COLLECTION_ADD_API.getDescription());
        aConstructorVisitor.visitInsn(Opcodes.POP);

        aConstructorVisitor.visitVarInsn(Opcodes.ALOAD, 0);
        aConstructorVisitor.visitFieldInsn(Opcodes.GETFIELD, anOwner, possibleValuesFieldName, FormGeneratorUtils.LIST_REFERENCE_NAME);
        aConstructorVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, AsmUtils.fromHighLevelClassName(dao), "getInstance", "()" + AsmUtils.getReferenceName(AsmUtils.fromHighLevelClassName(dao)));
        aConstructorVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, AsmUtils.fromHighLevelClassName(dao), "retrieveAll", "()" + FormGeneratorUtils.COLLECTION_REFERENCE_NAME);
        aConstructorVisitor.visitMethodInsn(Opcodes.INVOKEINTERFACE, FormGeneratorUtils.LIST_CLASS_NAME, "addAll", "(" + FormGeneratorUtils.COLLECTION_REFERENCE_NAME + ")" + TypeDescription.BOOLEAN);
        aConstructorVisitor.visitInsn(Opcodes.POP);
      }
    }

    protected Collection<Attribute> getFormAttributes(InputStream anInputStream) throws IOException
    {
      Collection<Attribute> formAttributes = new Vector<>();

      try
      {
        Source source = new Source(anInputStream);
        for (Element element : source.getAllElements())
        {
          String name = element.getAttributeValue("name");
          if (name != null)
          {
            Class elementType = getElementType(element);
            if (elementType != null)
            {
              Attribute attribute = new DefaultAttribute(name, elementType);
              attribute.setProperty("dao", element.getAttributeValue("dao"));
              formAttributes.add(attribute);
            }
          }
        }
      }
      catch (ClassNotFoundException anException)
      {
        throw new IOException("Unable to generate form", anException);
      }
      return formAttributes;
    }

    protected Class getElementType(Element anElement) throws ClassNotFoundException
    {

      String attributeType = anElement.getAttributeValue("attribute-type");
      if (attributeType != null)
      {
        return Constants.forName(attributeType);
      }
      else if ("input".equals(anElement.getName()))
      {
        String type = anElement.getAttributeValue("type");
        if ("text".equals(type))
        {
          return String.class;
        }
        else if ("number".equals(type))
        {
          return Integer.class;
        }
      }
      return null;
    }

    static public void main(String[] theArguments) throws Exception
    {
      FormGenerator formGenerator = new JerichoFormGenerator();
      String name = "generated/TypesForm";
      File file = new File("WEB-INF/classes/generated");
      file.mkdir();
      file = new File(file, "TypesForm.class");
      try (InputStream inputStream = new FileInputStream("pages/types.jsp"); OutputStream outputStream = new FileOutputStream(file);)
      {
        formGenerator.generateForm(name, inputStream, outputStream);
      }
    }
  }
}
